//! Proc macros used by Ruffle to generate various boilerplate.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse_macro_input, parse_quote, FnArg, ImplItem, ImplItemMethod, ItemEnum, ItemTrait, Pat,
    TraitItem, Visibility,
};

/// `enum_trat_object` will define an enum whose variants each implement a trait.
/// It can be used as faux-dynamic dispatch. This is used as an alternative to a
/// trait object, which doesn't get along with GC'd types.
///
/// This will auto-implement the trait for the enum, delegating all methods to the
/// underlying type. Additionally, `From` will be implemented for all of the variants,
/// so an underlying type can easily be converted into the enum.
///
/// TODO: This isn't completely robust for all cases, but should be good enough
/// for our usage.
///
/// Usage:
/// ```
/// use ruffle_macros::enum_trait_object;
///
/// #[enum_trait_object(
///     pub enum MyTraitEnum {
///         Object(Object)
///     }
/// )]
/// trait MyTrait {}
///
/// struct Object {}
/// impl MyTrait for Object {}
/// ```
#[proc_macro_attribute]
pub fn enum_trait_object(args: TokenStream, item: TokenStream) -> TokenStream {
    // Parse the input.
    let input_trait = parse_macro_input!(item as ItemTrait);
    let trait_name = input_trait.ident.clone();
    let trait_generics = input_trait.generics.clone();
    let enum_input = parse_macro_input!(args as ItemEnum);
    let enum_name = enum_input.ident.clone();

    if trait_generics.lifetimes().count() > 1 {
        panic!("Only one lifetime parameter is currently supported");
    }

    if trait_generics.type_params().count() > 1 {
        panic!("Generic type parameters are currently unsupported");
    }

    if trait_generics != enum_input.generics {
        panic!("Trait and enum should have the same generic parameters");
    }
    // Implement each trait. This will match against each enum variant and delegate
    // to the underlying type.
    let trait_methods: Vec<_> = input_trait
        .items
        .iter()
        .map(|item| match item {
            TraitItem::Method(method) => {
                let method_name = method.sig.ident.clone();
                let params: Vec<_> = method
                    .sig
                    .inputs
                    .iter()
                    .filter_map(|arg| {
                        if let FnArg::Typed(arg) = arg {
                            if let Pat::Ident(i) = &*arg.pat {
                                let arg_name = i.ident.clone();
                                return Some(quote!(#arg_name,));
                            }
                        }
                        None
                    })
                    .collect();

                let match_arms: Vec<_> = enum_input
                    .variants
                    .iter()
                    .map(|variant| {
                        let variant_name = variant.ident.clone();
                        quote! {
                            #enum_name::#variant_name(o) => o.#method_name(#(#params)*),
                        }
                    })
                    .collect();
                let method_block = quote!({
                    match self {
                        #(#match_arms)*
                    }
                });

                ImplItem::Method(ImplItemMethod {
                    attrs: method.attrs.clone(),
                    vis: Visibility::Inherited,
                    defaultness: None,
                    sig: method.sig.clone(),
                    block: parse_quote!(#method_block),
                })
            }
            _ => panic!("Unsupported trait item: {:?}", item),
        })
        .collect();

    let (impl_generics, ty_generics, where_clause) = trait_generics.split_for_impl();

    // Implement `From` for each variant type.
    let from_impls: Vec<_> = enum_input
        .variants
        .iter()
        .map(|variant| {
            let variant_name = variant.ident.clone();
            let variant_type = variant
                .fields
                .iter()
                .next()
                .expect("Missing field for enum variant")
                .ty
                .clone();

            quote!(
                impl #impl_generics From<#variant_type> for #enum_name #ty_generics {
                    fn from(obj: #variant_type) -> #enum_name #trait_generics {
                        #enum_name::#variant_name(obj)
                    }
                }
            )
        })
        .collect();

    let out = quote!(
        #input_trait

        #enum_input

        impl #impl_generics #trait_name #ty_generics for #enum_name #ty_generics #where_clause {
            #(#trait_methods)*
        }

        #(#from_impls)*
    );

    out.into()
}
